import type { Intersection, Object3D } from 'three';
import { computed, reactive, ref } from 'vue';
import { uniqueBy } from '../utils';
import { useRaycaster } from './useRaycaster';

type CallbackFn = ( intersection: Intersection, event: PointerEvent ) => void;
type CallbackFnPointerLeave = ( object: Object3D, event: PointerEvent ) => void;

export interface EventProps {
    onpointerdown?: CallbackFn;
    onpointerenter?: CallbackFn;
    onpointermove?: CallbackFn;
    onpointerleave?: CallbackFnPointerLeave;
}

export const usePointerEventHandler = (
    { scene, contextParts }
) => {

    const objectsWithEventListeners = reactive( {
        click: new Map<Object3D, CallbackFn>(),
        pointerMove: new Map<Object3D, CallbackFn>(),
        pointerEnter: new Map<Object3D, CallbackFn>(),
        pointerLeave: new Map<Object3D, CallbackFnPointerLeave>(),
    } );

    const blockingObjects = ref( new Set<Object3D>() );

    const registerBlockingObject = ( object: Object3D ) => {
        blockingObjects.value.add( object );
    };

    const deregisterBlockingObject = ( object: Object3D ) => {
        blockingObjects.value.delete( object );
    };

    const deregisterObject = ( object: Object3D ) => {
        Object.values( objectsWithEventListeners ).forEach( map => map.delete( object ) );
        deregisterBlockingObject( object );
    };

    const registerObject = ( object: Object3D & EventProps ) => {
        console.log( object );

        const { onpointerdown, onpointermove, onpointerenter, onpointerleave } = object;

        if ( onpointerdown ) objectsWithEventListeners.click.set( object, onpointerdown );
        if ( onpointermove ) objectsWithEventListeners.pointerMove.set( object, onpointermove );
        if ( onpointerenter ) objectsWithEventListeners.pointerEnter.set( object, onpointerenter );
        if ( onpointerleave ) objectsWithEventListeners.pointerLeave.set( object, onpointerleave );
    };

    // to make the registerObject available in the custom renderer (nodeOps), it is attached to the scene
    scene.userData.registerAtPointerEventHandler = registerObject;
    scene.userData.deregisterAtPointerEventHandler = deregisterObject;

    scene.userData.registerBlockingObjectAtPointerEventHandler = registerBlockingObject;
    scene.userData.deregisterBlockingObjectAtPointerEventHandler = deregisterBlockingObject;

    const objectsToWatch = computed( () =>
        uniqueBy(
            [
                ...Array.from( blockingObjects.value ),
                ...Object.values( objectsWithEventListeners )
                    .map( map => Array.from( map.keys() ) )
                    .flat(),
            ],
            ( { uuid } ) => uuid,
        ),
    );

    const { onClick, onPointerMove } = useRaycaster( objectsToWatch, contextParts );

    onClick( ( { intersects, event } ) => {
        if ( intersects.length ) objectsWithEventListeners.click.get( intersects[ 0 ].object )?.( intersects[ 0 ], event );
    } );

    let previouslyIntersectedObject: Object3D | null;

    onPointerMove( ( { intersects, event } ) => {
        const firstObject = intersects?.[ 0 ]?.object;

        const { pointerLeave, pointerEnter, pointerMove } = objectsWithEventListeners;

        if ( previouslyIntersectedObject && previouslyIntersectedObject !== firstObject )
            pointerLeave.get( previouslyIntersectedObject )?.( previouslyIntersectedObject, event );

        if ( firstObject ) {
            if ( previouslyIntersectedObject !== firstObject ) pointerEnter.get( firstObject )?.( intersects[ 0 ], event );

            pointerMove.get( firstObject )?.( intersects[ 0 ], event );
        }

        previouslyIntersectedObject = firstObject || null;
    } );

    return {
        registerObject,
        deregisterObject,
    };
};
